// Copyright (C) Kumo inc. and its affiliates.
// Author: Jeff.li lijippy@163.com
// All rights reserved.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

package cmd

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"syscall"

	"github.com/fatih/color"
	"github.com/google/uuid"
	perrs "github.com/kumose/errors"
	"github.com/kumose/kmopt/pkg/environment"
	kmoptexec "github.com/kumose/kmopt/pkg/exec"
	"github.com/kumose/kmopt/pkg/localdata"
	logprinter "github.com/kumose/kmopt/pkg/logger/printer"
	"github.com/kumose/kmopt/pkg/repository"
	"github.com/kumose/kmopt/pkg/repository/v1manifest"
	"github.com/kumose/kmopt/pkg/status"
	"github.com/kumose/kmopt/pkg/version"
	"github.com/spf13/cobra"
	"github.com/ttacon/chalk"
)

var (
	isConsole bool

	terminal = status.TerminalOutput{}
	console  = status.RawOutput{}

	GlobalOutput status.StatusOutput = &terminal
)

var (
	repoOpts  repository.Options
	eventUUID = uuid.New().String()
	log       = logprinter.NewLogger("")

	rootCmd *cobra.Command
)

var (
	binPath string
	tag     string
)

var greetCmd = &cobra.Command{
	Use:   "greet",
	Short: "Print a greeting message",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(chalk.Green.Color("Hello, friend! Welcome to kmopt."))
		i := status.NewOk("this is a info")
		GlobalOutput.Print(i)
	},
}

var warnCmd = &cobra.Command{
	Use:   "warn",
	Short: "Print a greeting message",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(chalk.Green.Color("Hello, friend! Welcome to kmopt."))
		i := status.NewWarn(1, "this is a warn")
		GlobalOutput.Print(i)
	},
}

var errorCmd = &cobra.Command{
	Use:   "error",
	Short: "Print a greeting message",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(chalk.Green.Color("Hello, friend! Welcome to kmopt."))
		i := status.NewError(1, "this is a error")
		GlobalOutput.Print(i)
	},
}

var fatalCmd = &cobra.Command{
	Use:   "fatal",
	Short: "Print a greeting message",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(chalk.Green.Color("Hello, friend! Welcome to kmopt."))
		i := status.NewFatal(1, "this is a fatal")
		GlobalOutput.Print(i)
	},
}

func init() {

	cobra.EnableCommandSorting = false
	_ = os.Setenv(localdata.EnvNameTelemetryEventUUID, eventUUID)

	rootCmd = &cobra.Command{
		Use: `kmopt [flags] <command> [args...]
	kmopt [flags] <component> [args...]`,
		Long: `kmopt is a command-line component management tool that can help to download and install
	TiDB platform components to the local system. You can run a specific version of a component via
	"kmopt <component>[:version]". If no version number is specified, the latest version installed
	locally will be used. If the specified component does not have any version installed locally,
	the latest stable version will be downloaded from the repository.`,
		Example: `  $ kmopt playground                    # Quick start
	$ kmopt playground nightly            # Start a playground with the latest nightly version
	$ kmopt install <component>[:version] # Install a component of specific version
	$ kmopt update --all                  # Update all installed components to the latest version
	$ kmopt update --nightly              # Update all installed components to the nightly version
	$ kmopt update --self                 # Update the "kmopt" to the latest version
	$ kmopt list                          # Fetch the latest supported components list
  	$ kmopt status                        # Display all running/terminated instances
  	$ kmopt clean <name>                  # Clean the data of running/terminated instance (Kill process if it's running)
  	$ kmopt clean --all                   # Clean the data of all running/terminated instances`,

		SilenceErrors: true,

		DisableFlagParsing: true,

		Args: func(cmd *cobra.Command, args []string) error {
			// Support `kmopt <component>`
			return nil
		},

		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			if isConsole {
				GlobalOutput = &console
			} else {
				GlobalOutput = &terminal
			}
			switch cmd.Name() {
			case "init", "rotate", "set":
				if cmd.HasParent() && cmd.Parent().Name() == "mirror" {
					// skip environment init
					break
				}
				fallthrough
			default:
				e, err := environment.InitEnv(repoOpts, repository.MirrorOptions{})
				if err != nil {
					if errors.Is(perrs.Cause(err), v1manifest.ErrLoadManifest) {
						log.Warnf("Please check for root manifest file, you may download one from the repository mirror, or try `kmopt mirror set` to force reset it.")
					}
					return err
				}
				environment.SetGlobalEnv(e)
			}
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) == 0 {
				return cmd.Help()
			}
			env := environment.GlobalEnv()

			// TBD: change this flag to subcommand

			// We assume the first unknown parameter is the component name and following
			// parameters will be transparent passed because registered flags and subcommands
			// will be parsed correctly.
			// e.g: kmopt --tag mytag --rm playground --db 3 --pd 3 --kv 4
			//   => run "playground" with parameters "--db 3 --pd 3 --kv 4"
			// kmopt --tag mytag --binpath /xxx/tikv-server tikv
			switch args[0] {
			case "--help", "-h":
				return cmd.Help()
			case "--version", "-v":
				fmt.Println(version.NewKmoptVersion().String())
				return nil
			case "--binary":
				if len(args) < 2 {
					return fmt.Errorf("flag needs an argument: %s", args[0])
				}
				component, ver := environment.ParseCompVersion(args[1])
				selectedVer, err := env.SelectInstalledVersion(component, ver)
				if err != nil {
					return err
				}
				binaryPath, err := env.BinaryPath(component, selectedVer)
				if err != nil {
					return err
				}
				fmt.Println(binaryPath)
				return nil
			case "--binpath":
				if len(args) < 2 {
					return fmt.Errorf("flag %s needs an argument", args[0])
				}
				binPath = args[1]
				args = args[2:]
			case "--tag", "-T":
				if len(args) < 2 {
					return fmt.Errorf("flag %s needs an argument", args[0])
				}
				tag = args[1]
				args = args[2:]
			}

			// component may use tag from environment variable. as workaround, make kmopt set the same tag
			for i := 0; i < len(args)-1; i++ {
				if args[i] == "--tag" || args[i] == "-T" {
					tag = args[i+1]
				}
			}

			if len(args) < 1 {
				return cmd.Help()
			}

			componentSpec := args[0]
			args = args[1:]
			if len(args) > 0 && args[0] == "--" {
				args = args[1:]
			}

			return kmoptexec.RunComponent(env, tag, componentSpec, binPath, args)
		},
		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
			return environment.GlobalEnv().Close()
		},
		SilenceUsage: true,

		// implement auto completion for kmopt components
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			env := environment.GlobalEnv()
			if len(args) == 0 {
				var result []string
				installed, _ := env.Profile().InstalledComponents()
				for _, comp := range installed {
					if strings.HasPrefix(comp, toComplete) {
						result = append(result, comp)
					}
				}
				return result, cobra.ShellCompDirectiveNoFileComp
			}

			component, version := environment.ParseCompVersion(args[0])

			selectedVer, err := env.SelectInstalledVersion(component, version)
			if err != nil {
				return nil, cobra.ShellCompDirectiveNoFileComp
			}
			binaryPath, err := env.BinaryPath(component, selectedVer)
			if err != nil {
				return nil, cobra.ShellCompDirectiveNoFileComp
			}

			argv := []string{binaryPath, "__complete"}
			argv = append(append(argv, args[1:]...), toComplete)
			_ = syscall.Exec(binaryPath, argv, os.Environ())
			return nil, cobra.ShellCompDirectiveNoFileComp
		},
	}
	rootCmd.PersistentFlags().BoolVarP(&isConsole, "console", "c", false, "disable colored console output")

	// useless, exist to generate help information
	rootCmd.Flags().String("binary", "", "Print binary path of a specific version of a component `<component>[:version]`\n"+
		"and the latest version installed will be selected if no version specified")
	rootCmd.Flags().StringP("tag", "T", "", "[Deprecated] Specify a tag for component instance")
	rootCmd.Flags().String("binpath", "", "Specify the binary path of component instance")
	rootCmd.Flags().BoolP("version", "v", false, "Print the version of kmopt")

	rootCmd.AddCommand(greetCmd)
	rootCmd.AddCommand(warnCmd)
	rootCmd.AddCommand(errorCmd)
	rootCmd.AddCommand(fatalCmd)
	rootCmd.AddCommand(
		newEnvCmd(),
		newHistoryCmd(),
		newCleanCmd(),
		newLinkCmd(),
		newUnlinkCmd(),
		newInstallCmd(),
		newUninstallCmd(),
		newUpdateCmd(),
		newStatusCmd(),
		newListCmd(),
		newMirrorCmd(),
	)
}

func Execute() int {

	code := 0

	err := rootCmd.Execute()
	if err != nil {
		// use exit code from component
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			code = exitErr.ExitCode()
		} else {
			fmt.Fprintln(os.Stderr, color.RedString("Error: %v", err))
			code = 1
		}
	}

	color.Unset()

	return code
}
